// Copyright 2011 Douglas Lacher, All Rights Reserved.

package com.devioustoast.turretdroid;

import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.RadialGradient;
import android.graphics.Shader;
import android.graphics.drawable.BitmapDrawable;
import android.hardware.Camera;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.os.Bundle;
import android.os.Environment;
import android.util.FloatMath;
import android.util.Log;
import android.view.Display;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.RelativeLayout;
import android.widget.SeekBar;
import android.widget.Spinner;
import android.widget.TextView;

/**
 * This is the main class for the TurretDroid.
 *
 * @author Douglas Lacher <doug.lacher@gmail.com>
 *
 */
public class TurretDroidActivity
    extends Activity implements SensorEventListener {
  // Constants.
  private static final String LOG_TAG
      = TurretDroidActivity.class.getSimpleName();
  private static final String TURRET_DROID_BASE_PATH = "turret_droid";
  private static final String NORMAL_SOUNDS_PATH = "turret_floor";
  private static final String DEFECTIVE_SOUNDS_PATH = "turret_defective";
  private static final String OPERA_SOUNDS_PATH = "turret_opera";
  private static final float GRAVITY = 9.81f;
  private static final float ALMOST_GRAVITY = 9.4f;
  private static final MediaPlayer MEDIA_PLAYER = new MediaPlayer();

  // Member variables.
  private Camera mCamera;
  private SensorManager mSensorManager;
  private Sensor mAccelerometer;
  private Map<TurretType, Turret> mTurrets = new HashMap<TurretType, Turret>();
  private TurretType mCurrentTurret;
  private CameraProducer mCameraProducer;
  private CameraConsumer mCameraConsumer;
  private BlockingDeque<byte[]> mPictureRole
      = new LinkedBlockingDeque<byte[]>(10);

  // UI widgets.
  private TextView mTextErrorView;
  private RelativeLayout mLayoutMain;
  private RelativeLayout mLayoutControl;
  private Spinner mSpinnerTurretList;
  private ArrayAdapter<TurretType> mAdapterTurretList;
  private Button mButtonStopPlaying;
  private Button mButtonHideUI;
  private SeekBar mSeekBarCameraSensitivity;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    
    setupTurrets();
    setupUI();
    setupCamera();
    
    // Setup sensors and hardware devices.
    mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
    mAccelerometer = mSensorManager.getDefaultSensor(
        Sensor.TYPE_ACCELEROMETER);
    MEDIA_PLAYER.setAudioStreamType(AudioManager.STREAM_MUSIC);
    setVolumeControlStream(AudioManager.STREAM_MUSIC);
    
    setupCameraMonitor();
  }

  @Override
  protected void onPause() {
    super.onPause();
    mSensorManager.unregisterListener(this);
    if (MEDIA_PLAYER.isPlaying()) {
      MEDIA_PLAYER.stop();
    }
    releaseCamera();
  }

  private void releaseCamera() {
    mCameraProducer.stop();
    mCameraConsumer.stop();
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      Log.e(
          LOG_TAG,
          "Sleep interrupted while waiting for camera producer and " +
          "consumer threads to stop.",
          e);
    }
    if (mCamera != null) {
      mCamera.release();
      mCamera = null;
    }
  }

  @Override
  protected void onResume() {
    super.onResume();
    mSensorManager.registerListener(
        this, mAccelerometer, SensorManager.SENSOR_DELAY_NORMAL);
    setupCamera();
  }

  /**
   * Sets up the Turret objects.  Requires that external storage is available.
   * After all Turrets are created, mCurrentTurret is set to NORMAL.
   */
  private void setupTurrets() {
    if (isExternalStorageAvailable()) {
      File externalStorageRoot = Environment.getExternalStorageDirectory();
      if(externalStorageRoot.canRead()) {
        File turretDroidBaseDir = new File(
            externalStorageRoot.getAbsolutePath(), TURRET_DROID_BASE_PATH);
        if (turretDroidBaseDir.exists() && turretDroidBaseDir.canRead()) {
          mTurrets.put(
              TurretType.NORMAL,
              createNormalTurret(
                  new File(turretDroidBaseDir, NORMAL_SOUNDS_PATH),
                  MEDIA_PLAYER));
          mTurrets.put(
              TurretType.DEFECTIVE,
              createDefectiveTurret(
                  new File(turretDroidBaseDir, DEFECTIVE_SOUNDS_PATH),
                  MEDIA_PLAYER));
          mTurrets.put(
              TurretType.OPERA,
              createOperaTurret(
                  new File(turretDroidBaseDir, OPERA_SOUNDS_PATH),
                  MEDIA_PLAYER));
          mCurrentTurret = TurretType.NORMAL;
        }
      }
    }
  }

  /**
   * Sets up the UI.  Must be called after setupTurrets().
   */
  private void setupUI() {
    setContentView(R.layout.main);
    // Setup UI widgets in main.xml.
    mTextErrorView = (TextView) findViewById(R.id.error_field);
    mTextErrorView.setVisibility(View.GONE);
    mLayoutMain = (RelativeLayout) findViewById(R.id.layout_main);
    mLayoutControl = (RelativeLayout) findViewById(R.id.layout_control);
    setupEyeBackground();
    setupControlView();
  }

  /**
   * Sets up the turret eye graphics.  Must be called at the end of setupUI().
   */
  private void setupEyeBackground() {
    Display display = getWindowManager().getDefaultDisplay();
    Point size = new Point();
    display.getSize(size);
    float halfX = FloatMath.ceil(size.x / 2);
    float halfY = FloatMath.ceil(size.y / 2);
    float radius = FloatMath.floor(Math.min(size.x, size.y) / 2);
    
    RadialGradient gradient = new RadialGradient(
        halfX, halfY, radius, Color.RED, Color.BLACK, Shader.TileMode.CLAMP);
    Paint paint = new Paint();
    paint.setDither(true);
    paint.setShader(gradient);
    
    Bitmap bitmap = Bitmap.createBitmap(
        size.x, size.y, Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(bitmap);
    canvas.drawCircle(halfX, halfY, radius, paint);

    mLayoutMain.setBackgroundDrawable(new BitmapDrawable(bitmap));
    mLayoutMain.setOnClickListener(new OnClickListener() {
      public void onClick(View v) {
        mLayoutControl.setVisibility(View.VISIBLE);
      }});
    // TODO(dlacher): animate the eye.
  }

  /**
   * Sets up the UI control widgets.  Must be called at the end of setupUI().
   */
  private void setupControlView() {
    mSpinnerTurretList = (Spinner) findViewById(R.id.spinner_turret_list);
    mAdapterTurretList = new ArrayAdapter<TurretType>(
        this, R.layout.textview_centered);
    mAdapterTurretList.setDropDownViewResource(
        R.layout.textview_centered);
    mAdapterTurretList.addAll(mTurrets.keySet());
    mSpinnerTurretList.setAdapter(mAdapterTurretList);
    mSpinnerTurretList.setOnItemSelectedListener(new OnItemSelectedListener() {
      public void onItemSelected(
          AdapterView<?> parent, View view, int position, long id) {
        Log.d(LOG_TAG, String.format(
            "Selecting %s turret.", mAdapterTurretList.getItem(position)));
        setCurrentTurret(mAdapterTurretList.getItem(position));
      }
      public void onNothingSelected(AdapterView<?> adapterView) {
        Log.d(LOG_TAG, "No turret selected.  Selecting NORMAL turret.");
        setCurrentTurret(TurretType.NORMAL);
      }
    });

    mButtonHideUI = (Button) findViewById(R.id.button_hide_ui);
    mButtonHideUI.setOnClickListener(new OnClickListener() {
      public void onClick(View v) {
        mLayoutControl.setVisibility(View.GONE);
      }
    });

    mButtonStopPlaying = (Button) findViewById(R.id.button_stop_playing);
    mButtonStopPlaying.setOnClickListener(new OnClickListener() {
      public void onClick(View v) {
        stopPlaying();
      }
    });

    mSeekBarCameraSensitivity = (SeekBar) findViewById(
        R.id.seekbar_camera_sensitivity);
    mSeekBarCameraSensitivity.setProgress(
        mSeekBarCameraSensitivity.getMax() / 2);
    mSeekBarCameraSensitivity.setOnSeekBarChangeListener(
        new SeekBar.OnSeekBarChangeListener() {
          public void onProgressChanged(
              SeekBar seekBar, int progress, boolean fromUser) {
            mCameraConsumer.setMaxImageVariance(
                progress / seekBar.getMax() * 0.20f);
          }
          public void onStopTrackingTouch(SeekBar seekBar) {
            // pass
          }
          public void onStartTrackingTouch(SeekBar seekBar) {
            // pass
          }
    });
  }

  /**
   * Sets up the mCamera.  Must be called after setupUI().
   */
  private void setupCamera() {
    if (mCamera == null) {
      mCamera = getFrontCameraInstance();
      mCamera.setDisplayOrientation(90);
    }
  }
  
  /**
   * Spawns threads to monitor the camera's view.  One thread periodically
   * takes a picture with the camera, and another thread polls the picture
   * roll to look for movement in the pictures.
   */
  private void setupCameraMonitor() {
    mCameraProducer = new CameraProducer(mCamera, mPictureRole);
    mCameraConsumer = new CameraConsumer(mPictureRole) {
      @Override
      public void callback() {
        mTurrets.get(mCurrentTurret).playRandomSoundForMovement();
      }
    };
    new Thread(mCameraProducer).start();
    new Thread(mCameraConsumer).start();
    Log.d(LOG_TAG, "Camera monitors started.");
  }

  /**
   * Required method for the SensorEventListener interface.
   */
  public void onAccuracyChanged(Sensor sensor, int accuracy) {
//    Log.d(
//        LOG_TAG,
//        "Accuracy changed for sensor " + sensor + " to " + accuracy + ".");
  }

  /**
   * Required method for the SensorEventListener interface.  The event is
   * passed to the current Turret to check for a matching event and play a
   * random sound for the event if matched.
   * 
   * @param event The sensor event.
   */
  public void onSensorChanged(SensorEvent event) {
    mTurrets.get(mCurrentTurret).playRandomSoundForEvent(event);
  }

  /**
   * Configures a "normal" floor turret.
   * 
   * @param directory where the sound file directories are found.
   * @param mediaPlayer the media player.
   * @return a Turret object.
   */
  private Turret createNormalTurret(File directory, MediaPlayer mediaPlayer) {
    Turret turret = new Turret(mediaPlayer);
    turret.setStartupSoundsPath(
        new File(directory.getPath(), "deploy").getPath());
    
    // Turret tipped over.
    turret.bindSoundsToEvent(
        new File(directory.getPath(), "tipped"),
        new Turret.ComparableSensorEvent(
            0, Sensor.TYPE_ACCELEROMETER,
            new float[]{-GRAVITY, -1.0f, -1.0f},
            new float[]{1.0f, 1.0f, 1.0f}));
    turret.bindSoundsToEvent(
        new File(directory.getPath(), "tipped"),
        new Turret.ComparableSensorEvent(
            0, Sensor.TYPE_ACCELEROMETER,
            new float[]{-1.0f, -1.0f, -GRAVITY},
            new float[]{1.0f, 1.0f, 1.0f}));
    turret.bindSoundsToEvent(
        new File(directory.getPath(), "tipped"),
        new Turret.ComparableSensorEvent(
            0, Sensor.TYPE_ACCELEROMETER,
            new float[]{-1.0f, -1.0f, -1.0f},
            new float[]{GRAVITY, 1.0f, 1.0f}));
    turret.bindSoundsToEvent(
        new File(directory.getPath(), "tipped"),
        new Turret.ComparableSensorEvent(
            0, Sensor.TYPE_ACCELEROMETER,
            new float[]{-1.0f, -1.0f, -1.0f},
            new float[]{1.0f, 1.0f, GRAVITY}));
    
    // Turret standing up-right.
    turret.bindSoundsToEvent(
        new File(directory.getPath(), "autosearch"),
        new Turret.ComparableSensorEvent(
            0, Sensor.TYPE_ACCELEROMETER,
            new float[]{-1.0f, ALMOST_GRAVITY, -1.0f},
            new float[]{1.0f, GRAVITY, 1.0f}));
    
    // Turret lifted vertically from an up-right position.
    turret.bindSoundsToEvent(
        new File(directory.getPath(), "pickup"),
        new Turret.ComparableSensorEvent(
            0, Sensor.TYPE_ACCELEROMETER,
            new float[]{-1.0f, GRAVITY + 1.0f, -1.0f},
            new float[]{1.0f, 20.0f, 1.0f}));

    // TODO(dlacher): Play "shotat" if turret senses a "jolt" on the accelerometer
    // TODO(dlacher): This won't work.  Need to setup an event for each dimension
    // of the axes.  Also do this for Prone.
//    turret.bindSoundsToEvent(
//        new File(directory.getPath(), "shotat"),
//        new Turret.ComparableSensorEvent(
//            0, Sensor.TYPE_ACCELEROMETER,
//            new float[]{-3.0f, -1.0f, -1.0f},
//            new float[]{3.0f, 1.0f, 1.0f}));
    
    // Turret "sees" movement ahead of it.
    turret.bindSoundsToEvent(
        new File(directory.getPath(), "active"),
        Turret.MOVEMENT_EVENT);
    
    // TODO(dlacher): Play a sound from "search" after target lost.
    
    // Need special handlers for "fizzler", "retire".
    // These may be all remote-controled.
    // Consider merging "disabled" sounds into "tipped".
    // Not sure if I'm going to do anything with "collide".
    return turret;
  }

  /**
   * Configures a "defective" floor turret.
   * 
   * @param directory where the sound file directories are found.
   * @param mediaPlayer the media player.
   * @return a Turret object.
   */
  private Turret createDefectiveTurret(
      File directory, MediaPlayer mediaPlayer) {
    Turret turret = new Turret(mediaPlayer);
    turret.setStartupSoundsPath(
        new File(directory.getPath(), "prerange").getPath());
    
    // Turret tipped over.
    turret.bindSoundsToEvent(
        new File(directory.getPath(), "postrange"),
        new Turret.ComparableSensorEvent(
            0, Sensor.TYPE_ACCELEROMETER,
            new float[]{-GRAVITY, -1.0f, -1.0f},
            new float[]{1.0f, 1.0f, 1.0f}));
    turret.bindSoundsToEvent(
        new File(directory.getPath(), "postrange"),
        new Turret.ComparableSensorEvent(
            0, Sensor.TYPE_ACCELEROMETER,
            new float[]{-1.0f, -1.0f, -GRAVITY},
            new float[]{1.0f, 1.0f, 1.0f}));
    turret.bindSoundsToEvent(
        new File(directory.getPath(), "postrange"),
        new Turret.ComparableSensorEvent(
            0, Sensor.TYPE_ACCELEROMETER,
            new float[]{-1.0f, -1.0f, -1.0f},
            new float[]{GRAVITY, 1.0f, 1.0f}));
    turret.bindSoundsToEvent(
        new File(directory.getPath(), "postrange"),
        new Turret.ComparableSensorEvent(
            0, Sensor.TYPE_ACCELEROMETER,
            new float[]{-1.0f, -1.0f, -1.0f},
            new float[]{1.0f, 1.0f, GRAVITY}));
    
    // Turret standing up-right.
    turret.bindSoundsToEvent(
        new File(directory.getPath(), "return"),
        new Turret.ComparableSensorEvent(
            0, Sensor.TYPE_ACCELEROMETER,
            new float[]{-1.0f, ALMOST_GRAVITY, -1.0f},
            new float[]{1.0f, GRAVITY, 1.0f}));
    
    // Turret lifted vertically from an up-right position.
    turret.bindSoundsToEvent(
        new File(directory.getPath(), "chat"),
        new Turret.ComparableSensorEvent(
            0, Sensor.TYPE_ACCELEROMETER,
            new float[]{-1.0f, GRAVITY + 1.0f, -1.0f},
            new float[]{1.0f, 20.0f, 1.0f}));
    
    // Turret "sees" movement ahead of it.
    turret.bindSoundsToEvent(
        new File(directory.getPath(), "test"),
        Turret.MOVEMENT_EVENT);
    
    return turret;
  }

  /**
   * Configures an "opera" floor turret.
   * 
   * @param directory where the sound file directories are found.
   * @param mediaPlayer the media player.
   * @return a Turret object.
   */
  private Turret createOperaTurret(File directory, MediaPlayer mediaPlayer) {
    Turret turret = new Turret(mediaPlayer);
    turret.setStartupSoundsPath(
        new File(directory.getPath(), "portal_still_alive").getPath());
//    turret.setStartupSoundsPath(
//        new File(directory.getPath(), "portal2_cara_mia_addio").getPath());
//    turret.setStartupSoundsPath(
//        new File(directory.getPath(), "portal2_robots_ftw").getPath());
//    turret.setStartupSoundsPath(
//        new File(directory.getPath(), "portal2_want_you_gone").getPath());
    return turret;
  }

  /**
   * Sets the error message in the UI and logs the exception.
   * 
   * @param errorMessage
   */
  public void setErrorMessage(Exception e) {
    mTextErrorView.setText("Exception: " + e.getMessage());
    mTextErrorView.setVisibility(View.VISIBLE);
    Log.e(LOG_TAG, Log.getStackTraceString(e));
  }

  /**
   * Clears the error message from the UI.
   */
  public void clearErrorMessage() {
    mTextErrorView.setText("");
    mTextErrorView.setVisibility(View.GONE);
  }

  /**
   * Checks that the external storage is mounted.
   * 
   * @return true if the external storage is mounted.
   */
  private Boolean isExternalStorageAvailable() {
    return Environment.MEDIA_MOUNTED.equals(
        Environment.getExternalStorageState());
  }

  /**
   * Gets an instance of the front-facing camera.
   * Example code from:
   * http://developer.android.com/guide/topics/media/camera.html#access-camera
   * 
   * @return A Camera instance.
   */
  private Camera getFrontCameraInstance() {
    Log.d(LOG_TAG, "Getting front camera.");
    Camera c = null;
    try {
      c = Camera.open(Camera.CameraInfo.CAMERA_FACING_FRONT);
    } catch (Exception e) {
      Log.e(LOG_TAG, "Could not get camera.", e);
    }
    return c;
  }

  /**
   * Stops the current turret's play back.
   */
  public void stopPlaying() {
    if (mTurrets.keySet().contains(mCurrentTurret)) {
      mTurrets.get(mCurrentTurret).stopPlaying();
    }
  }

  /**
   * Sets the current turret to the provided type.  The new type must be a
   * member of the mTurrets map.
   * 
   * @param turret the new turret type.
   */
  public void setCurrentTurret(TurretType turret) {
    if (mTurrets.keySet().contains(turret)) {
      stopPlaying();
      mCurrentTurret = turret;
      mSpinnerTurretList.setSelection(mAdapterTurretList.getPosition(turret));
      
      String path = mTurrets.get(mCurrentTurret).getStartupSoundsPath();
      if (path != null && !path.isEmpty()) {
        mTurrets.get(mCurrentTurret).playRandomSound(new File(path));
      }
    }
  }
}